<?php

Raise::load('core.RaiseStaticClass');
Raise::load('core.RaiseVariable');
Raise::load('core.RaiseFile');
Raise::load('core.RaiseDomTreeNode');
Raise::load('core.RaiseStack');

/**
 * RaiseDomParser class
 *
 * @author Sam-Mauris Yong <hellclanner at live dot com>
 * @license http://www.opensource.org/licenses/bsd-license New BSD License
 * @package Raise.Core.DOM
 * @since 1.2
 */
class RaiseDomParser extends RaiseStaticClass {

    /**
     * Parse a file and return the DOM tree
     * @param RaiseFile|string $file 
     */
    public static function parseFile($file){
        if(RaiseVariable::isType($file, RaiseVariable::TYPESTRING)){
            $file = new RaiseFile($file);
        }
        if($file instanceof RaiseFile){
            return self::parseString($file->getContent());
        }else{
            throw new RaiseDOMParseException('Could not parse a non-RaiseFile object in parseFile()');
        }
    }

    /**
     * Parse an entire document and return the DOM tree
     * @param string $doc
     * @return RaiseDomTreeNode
     */
    public static function parseDocument($doc){
        $doc = trim($doc);
        if(substr($doc, 0, 9) == '<!DOCTYPE' || substr($doc, 0, 5) == '<?xml'){
            // has DOCTYPE, drop it
            $pos = strpos($doc, '>');
            if($pos === false){
                // no ending > so throw exception
                throw new RaiseDOMParseException('Could not find matching closing ">".');
            }
            $doc = substr($doc, $pos);

        }
        return self::parseString($doc);
    }

    /**
     * Parse a string containing HTML
     * @param string $html
     * @return RaiseDomTreeNode
     */
    public static function parseString($html){
        $html = trim($html);
        $htmlLen = strlen($html);
        $parsePos = 0;
        $current = new RaiseDomTreeNode();
        for($startParsePos = $parsePos; $parsePos < $htmlLen; $parsePos++){
            $p = strpos($html, '<', $parsePos);
            if($p === false){
                break;
            }else{
                $parsePos = $p;
                // save whatever's before the tag into the tree node
//                if($parsePos - $startParsePos > 0){
//                    $frontContent = trim(substr($html, $startParsePos, $parsePos - $startParsePos));
//                    $tags->add($frontContent);
//                }

                // parse and get tag name
                $parsePos++;
                $tagName = null;
                for($startParsePos = $parsePos; $parsePos < $htmlLen; $parsePos++){
                    //$c = substr($html, $parsePos, 1);
                    $c = $html[$parsePos];
                    if($c == ' ' || $c == '>'){
                        $tagName = trim(substr($html, $startParsePos, $parsePos - $startParsePos));
                        break;
                    }
                }
                $value = '';
                $attrs = new RaiseCollection();
                if($c == '>'){
                    // no attributes to parse
                    $parsePos++;
                }else if($c == ' '){
                    for($startParsePos = $parsePos; $parsePos < $htmlLen; $parsePos++){
                        $c = $html[$parsePos];
                        if($c == '='){
                            $attrName = trim(substr($html, $startParsePos, $parsePos - $startParsePos));
                            $parsePos++;
                            $a = $html[$parsePos];
                            if($a == '"' || $a == '\''){
                                $parsePos++;
                                for($startParsePos = $parsePos; $parsePos < $htmlLen;$parsePos++){
                                    $c = $html[$parsePos];
                                    if($c == '\\'){ // escape character
                                        $parsePos++;
                                        $c = substr($html, $parsePos, 1);
                                        if($c == '\\'){
                                            $parsePos++;
                                        }
                                    }else if($c == $a){
                                        // end attribute variable
                                        $attrValue = stripslashes(substr($html, $startParsePos, $parsePos - $startParsePos));
                                        $parsePos++;
                                        break;
                                    }
                                }
                            }else{
                                $startParsePos = $parsePos;
                                $p = strpos($html, ' ', $parsePos);
                                if($p === false){
                                    $p = strpos($html, '>', $parsePos);
                                    if($p === false){
                                        throw new RaiseDOMParseException('Unexpect termination of string parsing, could not find the closing character of an attribute value (i.e. \' \' or \'>\' missing).');
                                        break;
                                    }
                                }
                                $parsePos = $p;
                                $attrValue = stripslashes(substr($html, $startParsePos, $parsePos - $startParsePos));
                            }
                            for($startParsePos = $parsePos; $parsePos < $htmlLen; $parsePos++){
                                $c = $html[$parsePos];
                                if($c == '>'){
                                    $parsePos--;
                                    break;
                                }
                                if($c != ' '){
                                    break;
                                }
                            }
                            $startParsePos = $parsePos;
                            $attrs->add($attrName, $attrValue);
                        }elseif($c == '>'){
                            break;
                        }
                    }
                    $parsePos++;
                }

                // finish parsing attributes
                // now parse value
                if(substr($html, $parsePos - 2, 2) != '/>'){
                    $stack = new RaiseStack();
                    for($startParsePos = $parsePos; $parsePos < $htmlLen; $parsePos++){
                        $spos = strpos($html, '<' . $tagName, $parsePos);
                        $epos = strpos($html, '</' . $tagName . '>', $parsePos);

                        if($spos === false){
                            $spos = -1;
                        }
                        if($epos === false){
                            $epos = -1;
                        }
                        if(($spos == -1 && $epos > -1) || ($spos > $epos)){
                            if($stack->count() == 0){
                                $value = stripslashes(substr($html, $startParsePos, $epos - $startParsePos));
                                $parsePos = $epos + strlen('</' . $tagName . '>');
                                break;
                            }else{
                                $parsePos = $epos + strlen('</' . $tagName . '>') - 1;
                                $stack->pop();
                            }
                        }elseif(($epos == -1 && $spos > -1) || ($epos > $spos)){
                            $parsePos = $spos + strlen('<' . $tagName) - 1;
                            $stack->push(1);
                        }else{
                            break;
                        }
                    }
                    $x = new RaiseDomTreeNode();
                    $tag = new RaiseHtmlTag($tagName, $value, $attrs);
                    $x->tag($tag);
                    if(!self::isValueCDATA($value) && !in_array($tagName, array('script', 'style'))){
                        $innerTags = self::parseString($value);
                        if($innerTags){
                            $innerTags->tag($tag);
                            $x = $innerTags;
                        }
                    }
                    $current->children()->add($x);
                }else{
                    $x = new RaiseDomTreeNode();
                    $tag = new RaiseHtmlTag($tagName, $value, $attrs);
                    $x->tag($tag);
                    $current->children()->add($x);
                }
                $startParsePos = $parsePos;
                $parsePos--;
            }
        }

        return $current;
    }

    /**
     * Check if the value is encapsulated in CDATA
     * @param string $value
     * @return boolean
     */
    private static function isValueCDATA($value){
        $value = trim($value);
        $cdatapos = strpos($value, '<![CDATA[');
        if($cdatapos !== false){
            $value = substr($value, 0, $cdatapos);
        }

        if(strpos($value,'<') !== false){
            return false;
        }
        return true;
    }
}